spb/market-atlas
Public
TypeScript 96.7%
SQL 1.6%
CSS 0.8%
JavaScript 0.5%
1import type { Metadata } from "next";2import Link from "next/link";3import { Kv, Page, PageHeader } from "@/components/ui/section";4import { StatusBadge } from "@/components/ui/status-badge";5import { api } from "@/lib/api";6import { EVENT_TYPE_LABEL, formatDateTime, instrumentHref } from "@/lib/format";7import type { MarketEvent } from "@/lib/types";89export const dynamic = "force-dynamic";1011export async function generateMetadata({ params }: { params: Promise<{ id: string }> }): Promise<Metadata> {12 const { id } = await params;13 try {14 const e = await api<MarketEvent>(`/v1/events/${encodeURIComponent(id)}`);15 return { title: e.title, description: e.summary ?? `${EVENT_TYPE_LABEL[e.type] ?? e.type} event recorded by Market Atlas.`, alternates: { canonical: `/events/${e.id}` }, robots: { index: false } };16 } catch {17 return { title: "Event" };18 }19}2021export default async function EventPage({ params }: { params: Promise<{ id: string }> }) {22 const { id } = await params;23 const e = await api<MarketEvent>(`/v1/events/${encodeURIComponent(id)}`);24 const url = typeof e.data.url === "string" ? (e.data.url as string) : null;25 return (26 <Page>27 <PageHeader28 kicker={29 <Link href={`/events?type=${e.type}`} className="hover:text-ink">30 Events · {EVENT_TYPE_LABEL[e.type] ?? e.type}31 </Link>32 }33 title={e.title}34 lead={e.summary ?? undefined}35 actions={<StatusBadge status={e.severity} />}36 />37 <div className="grid grid-cols-1 [&>*]:min-w-0 gap-6 lg:grid-cols-[1fr_320px]">38 <div className="rounded-md border border-rule bg-surface p-4">39 <h2 className="text-[11px] font-medium uppercase tracking-wide text-ink-3">Event data</h2>40 <Kv41 cols={1}42 className="mt-2"43 items={Object.entries(e.data)44 .filter(([, v]) => v != null && typeof v !== "object")45 .map(([k, v]) => [k.replace(/_/g, " "), String(v)] as [string, string])}46 />47 {Object.values(e.data).some((v) => v && typeof v === "object") && <pre className="mono mt-3 overflow-x-auto rounded bg-surface-2 p-3 text-xs">{JSON.stringify(Object.fromEntries(Object.entries(e.data).filter(([, v]) => v && typeof v === "object")), null, 2)}</pre>}48 {url && (49 <a href={url} target="_blank" rel="noopener noreferrer" className="mt-3 inline-block text-sm text-accent hover:underline">50 Open source document ↗51 </a>52 )}53 </div>54 <aside className="space-y-4">55 <div className="rounded-md border border-rule bg-surface p-4">56 <h2 className="text-[11px] font-medium uppercase tracking-wide text-ink-3">Provenance</h2>57 <Kv cols={1} className="mt-2" items={[["Time", formatDateTime(e.timestamp, { seconds: true })], ["Confidence", `${Math.round(e.confidence * 100)}%`], ["Independent sources", String(e.source_count)], ["Confirmed", e.confirmed_at ? formatDateTime(e.confirmed_at, { seconds: true }) : "single observation"], ["Event id", e.id]]} />58 <div className="mt-2 flex flex-wrap gap-1.5">59 {e.sources.map((s) => (60 <Link key={s} href={`/sources#${s}`} className="mono rounded border border-rule px-1.5 py-0.5 text-xs text-ink-2 hover:border-rule-strong">61 {s}62 </Link>63 ))}64 </div>65 </div>66 {e.instruments.length > 0 && (67 <div className="rounded-md border border-rule bg-surface p-4">68 <h2 className="text-[11px] font-medium uppercase tracking-wide text-ink-3">Instruments</h2>69 <ul className="mt-2 divide-y divide-rule text-sm">70 {e.instruments.map((i) => (71 <li key={i.id} className="py-1.5">72 <Link href={instrumentHref(i.id)} className="hover:underline">73 <span className="mono font-medium">{i.symbol ?? i.id}</span> {i.name && <span className="text-xs text-ink-3">{i.name}</span>}74 </Link>75 </li>76 ))}77 </ul>78 </div>79 )}80 </aside>81 </div>82 </Page>83 );84}85